// moon: The build system and package manager for MoonBit.
// Copyright (C) 2024 International Digital Economy Academy
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// For inquiries, you can contact us via e-mail at jichuruanjian@idea.edu.cn.

//! Constructs the build plan from the given build conditions, including
//! necessary pre-calculations.
//!
//! # Notes
//!
//! This module handles a different thing from the package dependency graph
//! generated by [`crate::solve`]. `solve` mainly constructs a dependency graph
//! that is not affected by what we're actually building, nor does it cares
//! about what actions are actually carried out on the dependency graph (like
//! check, build or test).
//!
//! In contrast, this module generates the actual build plan from a list of
//! input action nodes. Irrelevant packages are not included in this graph,
//! nor does irrelevant actions.
//!
//! The result of this module is analogous to Rust Cargo's [Compile unit graph][cu].
//! The reason why this is two graphs here in MoonBuild and only one graph in
//! Cargo is because our build procedure is a little more verbose than Cargo.
//!
//! # Precalculations
//!
//! This module pre-calculates shared information needed during build, including
//! file lists, hard-to-compute flags, and various configurations. The later
//! lowering pass is then merely just a dumb translation from existing config
//! and flags.
//!
//! [cu]: https://github.com/rust-lang/cargo/blob/master/src/cargo/core/compiler/unit.rs

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};

use log::{debug, info};
use moonutil::{compiler_flags::CC, cond_expr::OptLevel, mooncakes::ModuleId};
use petgraph::prelude::DiGraphMap;
use tracing::instrument;

use crate::{
    ResolveOutput,
    model::{BuildPlanNode, BuildTarget, PackageId, RunBackend},
    pkg_name::PackageFQNWithSource,
    prebuild::PrebuildOutput,
};

mod builders;
mod constructor;

use constructor::BuildPlanConstructor;

/// A directed graph representation of build dependencies and targets.
///
/// `BuildPlan` maintains a directed graph where nodes represent build
/// components and edges represent dependencies between them. It also stores
/// specifications for each build target, mapping targets to their detailed
/// configuration and requirements.
#[derive(Default)]
pub struct BuildPlan {
    /// The build dependency graph.
    ///
    /// Each node in this graph represents a step in building, and edges
    /// represent the dependencies between build steps, pointing **from each
    /// step to what it depends on**.
    graph: DiGraphMap<BuildPlanNode, FileDependencyKind>,

    /// The map of build target to its files and metadata.
    /// Used by nodes that require access to the raw MoonBit source files, like
    /// `Check`, `BuildCore`, `GenerateTestInfo` and `Format`.
    ///
    /// The following maps contain metadata needed for different types of build
    /// nodes. For each node present in the final graph, its metadata must
    /// already be present in the map.
    build_target_infos: HashMap<BuildTarget, BuildTargetInfo>,

    /// The map of build target to the metadata it needs when linking core
    link_core_info: HashMap<BuildTarget, LinkCoreInfo>,

    /// The map of build target to the C stubs information
    c_stubs_info: HashMap<PackageId, BuildCStubsInfo>,

    /// The map of build target to the information needed to make it executable
    make_executable_info: HashMap<BuildTarget, MakeExecutableInfo>,

    /// The map of package to its prebuild information, if any.
    prebuild_info: HashMap<PackageId, Vec<Option<PrebuildInfo>>>,

    /// The map of build target to its bundle information
    bundle_info: HashMap<ModuleId, BuildBundleInfo>,

    /// The nodes that were used as input to the build plan.
    input_nodes: Vec<BuildPlanNode>,
}

/// Additional information about a build plan's edge
///
/// # TODO
///
/// This is a temporary solution to determine which file is being depended on by
/// the consumer node.
///
/// A more generic solution might be involving creating associated types for
/// each node kind, specifying their output file list and order, and then
/// use a bitmap or index list to specify which files are being depended on.
#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)]
pub enum FileDependencyKind {
    /// Depending on all files available
    #[default]
    AllFiles,

    /// Depending on specific files of a `BuildCore` node.
    BuildCore { mi: bool, core: bool },

    /// Depending on specific files of a `GenerateTestInfo` node.
    GenerateTestInfo { meta: bool },
}

impl BuildPlan {
    /// Get the list of nodes that **the given node depends on**.
    pub fn dependency_nodes(
        &self,
        node: BuildPlanNode,
    ) -> impl Iterator<Item = BuildPlanNode> + '_ {
        self.graph
            .neighbors_directed(node, petgraph::Direction::Outgoing)
    }

    /// Get the dependencies of a node together with their edge kinds.
    pub fn dependency_edges(
        &self,
        node: BuildPlanNode,
    ) -> impl Iterator<Item = (BuildPlanNode, FileDependencyKind)> + '_ {
        // Use edges_directed to access edge weights without re-querying the graph.
        self.graph
            .edges_directed(node, petgraph::Direction::Outgoing)
            .map(move |(_, dep, kind)| (dep, *kind))
    }

    /// Get build target information for the given target.
    pub fn get_build_target_info(&self, target: &BuildTarget) -> Option<&BuildTargetInfo> {
        self.build_target_infos.get(target)
    }

    /// Get link core information for the given target.
    pub fn get_link_core_info(&self, target: &BuildTarget) -> Option<&LinkCoreInfo> {
        self.link_core_info.get(target)
    }

    /// Get C stubs information for the given target.
    pub fn get_c_stubs_info(&self, target: PackageId) -> Option<&BuildCStubsInfo> {
        self.c_stubs_info.get(&target)
    }

    /// Get make executable information for the given target.
    pub fn get_make_executable_info(&self, target: &BuildTarget) -> Option<&MakeExecutableInfo> {
        self.make_executable_info.get(target)
    }

    /// Get prebuild information for the given package.
    pub fn get_prebuild_info(&self, package: PackageId, idx: u32) -> Option<&PrebuildInfo> {
        self.prebuild_info
            .get(&package)
            .and_then(|v| v.get(idx as usize))
            .and_then(|x| x.as_ref())
    }

    /// Get bundle information for the given module.
    pub fn bundle_info(&self, module_id: ModuleId) -> Option<&BuildBundleInfo> {
        self.bundle_info.get(&module_id)
    }

    /// Get the list of nodes that **depend on the given node**.
    pub fn consumer_nodes(&self, node: BuildPlanNode) -> impl Iterator<Item = BuildPlanNode> + '_ {
        self.graph
            .neighbors_directed(node, petgraph::Direction::Incoming)
    }

    pub fn all_nodes(&self) -> impl Iterator<Item = BuildPlanNode> + '_ {
        self.graph.nodes()
    }

    pub fn node_count(&self) -> usize {
        self.graph.node_count()
    }

    pub fn input_nodes(&self) -> &[BuildPlanNode] {
        &self.input_nodes
    }
}

/// Common information about a moonbit package being built
#[derive(Debug)]
pub struct BuildTargetInfo {
    /// Regular compilation files
    pub(crate) regular_files: Vec<PathBuf>,

    /// Whitebox test files. Separated so determine whether the whitebox test is
    /// needed.
    ///
    /// TODO: suboptimal design. Better not populate unused build target info.
    pub(crate) whitebox_files: Vec<PathBuf>,

    /// Files that needs their doctests extracted instead of regular compilation.
    pub(crate) doctest_files: Vec<PathBuf>,

    /// The final list of warnings to pass to the compiler
    pub(crate) warn_list: Option<String>,
    /// The final list of alerts to pass to the compiler
    pub(crate) alert_list: Option<String>,

    /// Whether the target is designated by the user to not generate module interface.
    pub(crate) specified_no_mi: bool,

    /// The patch file to supply to this package
    pub(crate) patch_file: Option<PathBuf>,

    /// Check the `.mi` file against the given target. Used in virtual packages.
    /// Also implies that the target must not generate a `.mi` file.
    pub(crate) check_mi_against: Option<BuildTarget>,
    // we currently don't need this, as it's controlled by build-wise options
    // /// Whether compiling this target needs the standard library
    // pub std: bool,

    // we currently don't need this, as it's directly copied from mod.json.
    // pub is_main: bool,

    // Emit --enable-value-tracing if true
    pub(crate) value_tracing: bool,
}

impl BuildTargetInfo {
    pub fn files(&self) -> impl Iterator<Item = &Path> {
        self.regular_files
            .iter()
            .chain(self.whitebox_files.iter())
            .map(|x| x.as_path())
    }

    pub fn doctest_files(&self) -> impl Iterator<Item = &Path> {
        self.doctest_files.iter().map(|x| x.as_path())
    }

    pub fn no_mi(&self) -> bool {
        self.specified_no_mi
    }
}

#[derive(Debug)]
pub struct LinkCoreInfo {
    /// The targets in **initialization order**.
    pub(crate) linked_order: Vec<BuildTarget>,

    /// Whether this package overwrites `moonbitlang/core/abort`.
    ///
    /// This is needed since `abort` is a prebuilt package, and needs special
    /// treatment to not be rebuilt when building user code.
    ///
    /// MAINTAINERS: When touching this part again, check if there's a better way to do this.
    pub abort_overridden: bool,
    // we currently don't need this, as it's controlled by build-wise options
    // /// Whether linking this target needs the standard library
    // pub std: bool,
}

pub struct BuildCStubsInfo {
    /// The C compiler to compile the C stubs
    pub(crate) stub_cc: Option<CC>,
    /// Additional flags to pass to the C compiler when compiling the C stubs
    pub(crate) cc_flags: Vec<String>,
    /// Additional flags to pass to the linker (TCC only)
    #[allow(unused)]
    pub(crate) link_flags: Vec<String>,
}

pub struct MakeExecutableInfo {
    /// The C compiler to use to compile the package itself
    pub(crate) cc: Option<CC>,
    /// The flags to pass to the C compiler when compiling the package itself
    pub(crate) c_flags: Vec<String>,
    /// The C stub targets to link with.
    pub(crate) link_c_stubs: Vec<PackageId>,
}

/// Resolved information about a prebuild command.
pub struct PrebuildInfo {
    pub(crate) resolved_inputs: Vec<PathBuf>,
    pub(crate) resolved_outputs: Vec<PathBuf>,
    pub(crate) command: String,
}

/// Metadata about bundling the build output of different targets together.
pub struct BuildBundleInfo {
    /// The targets to bundle into the final bundle.
    pub(crate) bundle_targets: Vec<BuildTarget>,
}

/// Represents the environment in which the build is being performed.
pub struct BuildEnvironment {
    // FIXME: Target backend should go into the solver, not here
    pub target_backend: RunBackend,
    pub opt_level: OptLevel,
    /// Whether compiling requires the standard library.
    ///
    /// TODO: Move this to per-package/module.
    pub std: bool,
    /// Commandline_level warnings to enable/disable
    pub warn_list: Option<String>,
    pub alert_list: Option<String>,
    // Can have more, e.g. cross compile
}

/// Directives provided along the input actions.
#[derive(Debug, Default)]
pub struct InputDirective {
    /// Set `no_mi=true` for the given package.
    pub specify_no_mi_for: Option<PackageId>,
    /// Set the given patch file for the given package.
    ///
    /// MAINTAINERS: A target with `TargetKind::InlineTest` will look for
    /// the patch file in `TargetKind::Source` too.
    pub specify_patch_file: Option<(BuildTarget, PathBuf)>,

    /// Emit --enable-value-tracing for the given package's test targets. When
    /// `moon test -p pkg --enable-value-tracing`, this will be set to
    /// `Some(pkg)` so that only `pkg`'s test targets will emit
    /// --enable-value-tracing but not its dependencies' test targets.
    pub value_tracing: Option<PackageId>,
}

/// Represents errors that may occur during build graph construction.
#[derive(Debug, thiserror::Error)]
pub enum BuildPlanConstructError {
    #[error("Failed to set C compiler when compiling {1}")]
    FailedToSetCC(#[source] anyhow::Error, PackageFQNWithSource),
    #[error("Failed to set stub C compiler when compiling {1}")]
    FailedToSetStubCC(#[source] anyhow::Error, PackageFQNWithSource),
    #[error("Malformed cc flags in package {0}")]
    MalformedCCFlags(PackageFQNWithSource),
    #[error("Malformed cc link flags in package {0}")]
    MalformedCCLinkFlags(PackageFQNWithSource),
    #[error("Malformed stub cc flags in package {0}")]
    MalformedStubCCFlags(PackageFQNWithSource),
    #[error("Malformed stub cc link flags in package {0}")]
    MalformedStubCCLinkFlags(PackageFQNWithSource),

    #[error(
        "Package {package}'s (possibly transitive) dependency {dep} \
        is a virtual package with no default implementation, \
        and no override packages were given to implement it."
    )]
    NoImplementationForVirtualPackage {
        package: PackageFQNWithSource,
        dep: PackageFQNWithSource,
    },
}

/// Construct an abstract build graph from the given packages and input actions.
#[instrument(skip_all)]
pub fn build_plan(
    resolved: &ResolveOutput,
    build_env: &BuildEnvironment,
    input: impl Iterator<Item = BuildPlanNode>,
    input_directive: &InputDirective,
    prebuild_config: Option<&PrebuildOutput>,
) -> Result<BuildPlan, BuildPlanConstructError> {
    info!("Constructing build plan");
    debug!(
        "Build environment: backend={:?}, opt_level={:?}",
        build_env.target_backend, build_env.opt_level
    );

    let mut constructor =
        BuildPlanConstructor::new(resolved, build_env, input_directive, prebuild_config);
    constructor.build(input)?;
    let result = constructor.finish();

    info!(
        "Build plan construction completed with {} total nodes",
        result.node_count()
    );
    Ok(result)
}
